/*
   Copyright (C) 2009 Xizhi Zhu.  All Rights Reserved.

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions are met:

   1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
   2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
   3. The name of the author may not be used to endorse or promote products
   derived from this software without specific prior written permission.

   THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
   IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
   IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
   OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
   WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
   OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
   ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include "p2p-app.h"
#include "p2p-packet.h"
#include "p2p-param.h"

using namespace std;

static class P2PAppClass : public TclClass
{
public:
    P2PAppClass() : TclClass("Application/P2P/App") {}
    TclObject* create(int argc, const char*const* argv)
    {
        return (new P2PApp((Node*)TclObject::lookup(argv[4])));
    }
} class_p2p_app;

P2PApp::P2PApp(Node* node): RealApplication(node), tried_(true)
{
    connectionCheckTimer_ = new ConnectionCheckTimer(this);

    chunks_.resize(P2P_CHUNK_NUMBER);
    for (int i = 0; i < P2P_CHUNK_NUMBER; i++)
    {
        chunks_[i].available_ = P2P_CHUNK_NOT_AVAILABLE;
    }
}

P2PApp::~P2PApp()
{
    delete connectionCheckTimer_;
}

void P2PApp::recv(ADU* packet, nsaddr_t src)
{
    P2PPacket* p = (P2PPacket*)packet;

    updateTime(src);

    switch (p->type_)
    {
        case PktTypeRegReply:
            // successfully registered at the tracker
            connectionCheckTimer_->resched(0.0);
            break;

        case PktTypeNodeListReply:
            // node list received
            addNodeToList(p);
            chunkListRequest();
            break;

        case PktTypeChunkListRequest:
            // chunk list requested
            handleChunkListRequest(p);
            break;

        case PktTypeChunkListReply:
            // chunk list received
            handleChunkListReply(p);
            break;

        case PktTypeChunkRequest:
            // chunk requested
            handleChunkRequest(p);
            break;

        case PktTypeChunkReply:
            // received chunks
            handleChunkReply(p);
            break;

        default:
            break;
    }

    delete packet;
}

int P2PApp::command(int argc, const char*const* argv)
{
    if (2 == argc)
    {
        if (0 == strcmp(argv[1], "stop"))
        {
            // unregister
            stop();
            return TCL_OK;
        }
    }
    else if (3 == argc)
    {
        if (0 == strcmp(argv[1], "start"))
        {
            // start running
            tracker_ = (P2PTracker*)TclObject::lookup(argv[2]);
            start();
            return TCL_OK;
        }
    }

    return RealApplication::command(argc, argv);
}

void P2PApp::start()
{
    // send registration request to the tracker
    P2PPacket* p = new P2PPacket(PktTypeRegRequest);
    p->id_ = node_->address();
    p->isServer_ = false;

    send(p, tracker_->getNode()->address());
}

void P2PApp::stop()
{
    // send the unregistration request to the tracker
    P2PPacket* p = new P2PPacket(PktTypeUnregRequest);
    p->id_ = node_->address();
    p->isServer_ = false;

    send(p, tracker_->getNode()->address());
}

void P2PApp::updateTime(nsaddr_t src)
{
    int size = 0;
    int i = 0;

    // server list
    size = server_list_.size();
    for (i = 0; i < size; i++)
    {
        if (src == server_list_[i].id_)
        {
            server_list_[i].updated_ = Scheduler::instance().clock();
            return;
        }
    }

    // peer list
    size = server_list_.size();
    for (i = 0; i < size; i++)
    {
        if (src == server_list_[i].id_)
        {
            server_list_[i].updated_ = Scheduler::instance().clock();
            return;
        }
    }
}

void P2PApp::addNodeToList(P2PPacket* packet)
{
    PeerInfo info;
    info.connected_ = false;
    int size = 0;
    int size2 = 0;
    int i = 0;
    int j = 0;

    // add servers
    size = packet->server_list_.size();
    for (i = 0; i < size; i++)
    {
        size2 = server_list_.size();
        info.id_ = packet->server_list_[i];

        // check if it's itself
        if (node_->address() == info.id_)
        {
            continue;
        }

        // check if it's already in the list
        for (j = 0; j < size2; j++)
        {
            if (info.id_ == server_list_[i].id_)
            {
                break;
            }
        }
        if (j == size2)
        {
            server_list_.push_back(info);
        }
    }

    // add peers
    size = packet->peer_list_.size();
    for (i = 0; i < size; i++)
    {
        size2 = peer_list_.size();
        info.id_ = packet->peer_list_[i];

        // check if it's itself
        if (node_->address() == info.id_)
        {
            continue;
        }

        // check if it's already in the list
        for (j = 0; j < size2; j++)
        {
            if (info.id_ == peer_list_[i].id_)
            {
                break;
            }
        }
        if (j == size2)
        {
            peer_list_.push_back(info);
        }
    }
}

void P2PApp::checkConnection()
{
    // delete the timeouted node
    deleteOldNode();

    if (enoughConnectionSpeed())
    {
        // if the connection speed is enough
        tried_ = false;
        return;
    }

    // the current connection speed is not enough
    if (tried_)
    {
        // if we already tried to connect, request for new peers
        P2PPacket* p = new P2PPacket(PktTypeNodeListRequest);
        p->id_ = node_->address();
        send(p, tracker_->getNode()->address());
    }
    else
    {
        // request chunk list
        tried_ = true;
        chunkListRequest();
    }
}

void P2PApp::deleteOldNode()
{
    int size = 0;
    int i = 0;

    // server node
    size = server_list_.size();
    for (i = 0; i < size; i++)
    {
        if (P2P_SERVER_LIVING_TIME <
                Scheduler::instance().clock() - server_list_[i].updated_)
        {
            server_list_.erase(server_list_.begin() + i);
            size = server_list_.size();
        }
    }

    // peer node
    size = peer_list_.size();
    for (i = 0; i < size; i++)
    {
        if (P2P_PEER_LIVING_TIME <
                Scheduler::instance().clock() - peer_list_[i].updated_)
        {
            peer_list_.erase(peer_list_.begin() + i);
            size = peer_list_.size();
        }
    }
}

bool P2PApp::enoughConnectionSpeed() const
{
    // TODO check if the current connection speed is enough

    return false;
}

void P2PApp::chunkListRequest()
{
    int size = 0;
    int i = 0;

    // TODO request from all nodes, or just part of them?

    // request chunk list from servers
    size = server_list_.size();
    for (i = 0; i < size; i++)
    {
        P2PPacket* p = new P2PPacket(PktTypeChunkListRequest);
        p->id_ = node_->address();
        send(p, server_list_[i].id_);
    }

    // request chunk list from peers
    size = peer_list_.size();
    for (i = 0; i < size; i++)
    {
        P2PPacket* p = new P2PPacket(PktTypeChunkListRequest);
        p->id_ = node_->address();
        send(p, peer_list_[i].id_);
    }
}

void P2PApp::handleChunkListRequest(P2PPacket* packet)
{
    // TODO reply to all request, or ignore some?

    P2PPacket* p = new P2PPacket(PktTypeChunkListReply);
    p->id_ = node_->address();

    // put the index of the available chunks into the packet
    int size = chunks_.size();
    for (int i = 0; i < size; i++)
    {
        if (P2P_CHUNK_AVAILABLE == chunks_[i].available_)
        {
            p->available_chunks_.push_back(i);
        }
    }

    send(p, packet->id_);
}

void P2PApp::handleChunkListReply(P2PPacket* packet)
{
    // add the chunks info to local list
    int chunk_num = packet->available_chunks_.size();
    for (int i = 0; i < chunk_num; i++)
    {
        vector<nsaddr_t>& who_has
            = chunks_[packet->available_chunks_[i]].who_has_;

        // skip if it's already in the list
        int num = who_has.size();
        int j = 0;
        for (j = 0; j < num; j++)
        {
            if (packet->id_ == who_has[j])
            {
                break;
            }
        }
        if (j == num)
        {
            who_has.push_back(packet->id_);
        }
    }

    // request chunks
    chunkRequest();
}

void P2PApp::chunkRequest()
{
    // TODO request which chunk from which peer?

    for (int i = 0; i < P2P_CHUNK_NUMBER; i++)
    {
        // if it's being or already downloaded, or no one has this chunk
        // move to next
        if (P2P_CHUNK_NOT_AVAILABLE != chunks_[i].available_
            || 0 == chunks_[i].who_has_.size())
        {
            continue;
        }

        // request the chunk
        P2PPacket* p = new P2PPacket(PktTypeChunkRequest);
        p->id_ = node_->address();
        p->chunk_request_ = i;
        send(p, chunks_[i].who_has_[0]);
        chunks_[i].available_ = chunks_[i].who_has_[0];
    }
}

void P2PApp::handleChunkRequest(P2PPacket* packet)
{
    // TODO decide whether or not to reply

    P2PPacket* p = new P2PPacket(PktTypeChunkReply);
    p->id_ = node_->address();
    p->chunk_id_ = packet->chunk_request_;
    send(p, packet->id_);
}

void P2PApp::handleChunkReply(P2PPacket* packet)
{
    // TODO what to do if the peer doesn't want to reply?

    if (-1 == packet->chunk_id_)
    {
        return ;
    }

    // this chunk is received
    chunks_[packet->chunk_id_].available_ = P2P_CHUNK_AVAILABLE;
}

ConnectionCheckTimer::ConnectionCheckTimer(P2PApp* app)
    : TimerHandler(), app_(app)
{
}

void ConnectionCheckTimer::expire(Event*)
{
    app_->checkConnection();
    resched(P2P_CONNECTION_CHECK_INTERVAL);
}

